Skip to main content

Run the app

We're going to set up the Cubist chains and relayer and then interact with our contracts.

Start Cubist chains and relayer

Now that we've built our contracts, we want to deploy them and then interact with them. We're going to do this locally; the config (cubist-config.json) already specifies the URLs on which the chains will run. The app config will look something like this:

{
...
"network_profiles": {
"default": {
"ethereum": { "url": "http://127.0.0.1:8545" },
"polygon": { "url": "http://127.0.0.1:9545" }
}
}
}

You can alter the config to specify other URLs if you choose.

To start both the local chains and the Cubist relayer, use:

cubist start

Note that this command may take a while (i.e., a minute) the first time you run it. This is because Cubist needs to download and build the local chain running services before it can start the chains.

Once you've run cubist start, you'll see output that looks something like this:

Launching chains
ethereum ✔ [ 0s] [....................] http://localhost:8545/
polygon ✔ [ 1s] [....................] http://localhost:9545/ All servers available
Watching <path>/cubist-deploy dir

This output shows where the localnets are running (e.g., localhost:8545), and how long they took to become initialized and available. The output also lets us know that the local relayer is successfully watching for the events that tell it to relay information from one chain to another.

tip

Alternatively, you can start the chains and the relayer seperately with cubist chains start and cubist relayer start. For more information on these and other Cubist CLI commands, try cubist help, or check out the CLI reference.

caution

Right now, Avalanche and Avalanche-subnet localnets are slower to start up than other networks. We're working on it!

Run the app

Deploy

Deploy the contracts and the generated shims:

npm run deploy 17

> [email protected] deploy
> node ./src/deploy.js

Deployed StorageReceiver
to Ethereum @ 0xaddr
Polygon @ 0xaddr
Deployed StorageSender
to Polygon @ 0xaddr

The core of the deploy script that deploy invokes is this simple function (which uses the Cubist ORM generated at build time):

import {
CubistORM,
} from '../build/orm/index.js';
...

// Project instance
const cubist = new CubistORM();

// Contract factories
const StorageReceiver = cubist.StorageReceiver;
const StorageSender = cubist.StorageSender;

export async function deploy(val) {
// Deploy both the StorageReceiver contract and its shim
const receiver = await StorageReceiver.deploy(val);
const senderTarget = StorageSender.target();
...

// Deploy StorageSender (it has no shim)
const sender = await StorageSender.deploy(val, receiver.addressOn(senderTarget));
...

// Return the inner ethers.js contracts
return {
receiver: receiver.inner,
sender: sender.inner
};
}

The deploy script takes a value as its argument; when we invoked it, we supplied 17. First, the script deploys the StorageReceiver contract with val (17) as the constructor argument; it also implicitly deploys the StorageReceiver shim. Then, it deploys the StorageSender contract; since StorageSender is never called from a chain other than the one on which it is deployed, StorageSender has no shim.

The StorageSender deployment call (line 20) takes the receiver address as a constructor argument:

receiver.addressOn(senderTarget)

The addressOn call explicitly requests the address of the receiver contract on the sender chain---in other words, it requests the address of the StorageReceiver's shim. In this case, since senderTarget---StorageSenders's explicit target in the Cubist config file----is Polygon, this snippet of code gets the receiver contract's address on Polygon. If we were to change StorageSender's target chain, though, this code would still work; it's chain agnostic. Here's the chain-specific version of the code:

receiver.addressOn(Polygon)
tip

If you avoid hardcoding chain names in your off-chain code, you won't have to edit any code if you want to change chains. Instead, you'll be able to change chains by editing your configuration file.

caution

Our scripts assume that you only deploy each contract once. For now, the Cubist Node SDK only supports single deployment. If you want to deploy multiple instances of the same contract: reach out to [email protected], say hi on discord, or check out the Rust storage app example.

Run

We can now store, increment, decrement, and retrieve the counter values:

npm run inc 5

> node ./src/inc.js
sending StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)
Incremented counter
SENT StorageReceiver::store(16) (0xaddr@polygon -> 0xaddr@ethereum)

This command invokes the inc increment method to increment the counter by 5. Let's look at the core of the inc script that ultimately calls the smart contract function:

import { CubistORM, } from '../build/orm/index.js';
import { fileURLToPath, } from 'url';

// Project instance
const cubist = new CubistORM();

export async function inc(val) {
const sender = (await cubist.StorageSender.deployed()).inner;
await (await sender.inc(val)).wait(1);
...
}

Once again, this script creates a new Cubist instance. In line 8, it loads the StorageSender contract from its deployment receipt using the Cubist deployed function. Then, in the next line, it calls the deployed StorageSender contract's inc function using Cubist-generated bindings.

Now, when we retrieve the new value, the increment will be reflected on both chains:

npm run retrieve

> [email protected] retrieve
> node ./src/retrieve.js

Receiver counter (Ethereum) = 22
Sender counter (Polygon) = 22

dec and store work similarly.

Test the app

Shut down the Cubist local nets and relayer---because the CubistTestDK handles startup and shutdown automatically---and let's look at a simple test. First, the test code creates a new TestDK instance and sets up the chains and relayer to run before the tests start, and shut down after the tests end:

const testdk = new TestDK(CubistORM, { tmp_deploy_dir: true });
const cubist = testdk.cubist;

beforeAll(async () => {
await testdk.startService();
});

afterAll(async () => {
await testdk.stopService();
});

Now we can write the code that actually tests our application. The tests call inc, dec, and store, and then use retrieve to verify that the values have been updated correctly:

const val = 55;
const receiver = await cubist.StorageReceiver.deploy(val);
const senderTarget = cubist.StorageSender.target();
const sender = await cubist.StorageSender.deploy(val, receiver.addressOn(senderTarget));

...

// increment counter
expect(await (await sender.inner.inc(33)).wait(/* confirmations: */ 1)).to.not.throw;
// make sure sender value is incremented by 33
expect((await sender.inner.retrieve()).eq(val + 33)).is.true;
// check that the sender value is incremented by 33
// we "tryAFewTimes" because it may take a bit for the new value to be updated on the receiver
await tryAFewTimes(async () => {
expect((await receiver.inner.retrieve()).eq(val + 33)).is.true;
});
...

This test code uses cubist the same way that our application code did; all that's different is that we set up the chains and relayer programmatically in the tests, and via the command line in the actual application.

Let's run the test and see the test output:

npm test

The output shows that the chains and relayer, controlled by the TestDK code, actually started and stopped. It also shows the series of send and receive calls invoked by the test.

Shut down

Running cubist stop will shut down both the chains and the relayer. Once the chains and the relayer are shut down, trying to call contract functions from within your dapp will result in errors.